PEC 2 - Visualización de datos¶

Carlos José Ospina Arias¶

Packed Circle Chart¶

Descripción¶

Un Packed Circle chart nos permite representar estructuras jerárquicas en el que cada círculo répresenta un nodo de la dimension jerárquica y el tamaño del círculo se correlaciona con un atributo o medida vinculada a ese nodo. Es muy util para estructuras jerárquicas, pero puede ser difícil comparar los tamaños de los círculos, especialmente cuando están densamente empaquetados o en diferentes niveles jerárquicos.

Se utiliza principalmente para representar datos cuantitativos y cualitativos.

Aplicación práctica¶

Cargamos las librerías necesarías para la visualización de los datos.

In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import unicodedata
import circlify as circ
In [2]:
import warnings
warnings.filterwarnings('ignore')

Para nuestro juego de datos usaremos los datos disponibles en dades obertas de la Generalitat de Catalunya, en el cual se encuentran los presupuestos aprobados de la Generalitat de Catalunya para el ejercicio 2023.

https://analisi.transparenciacatalunya.cat/Economia/Pressupostos-aprovats-de-la-Generalitat-de-Catalun/yd9k-7jhw/about_data

In [3]:
# Cargamos el dataset
df_raw = pd.read_csv('Pressupostos_aprovats_de_la_Generalitat_de_Catalunya_20240415.csv')
In [4]:
df_raw.head()
Out[4]:
Subjecte/Àmbit Exercici Ingrés / Despesa Servei / Entitat Nom Servei / Entitat Subsector Codi Agrupació Nom Agrupació Codi Secció Nom secció ... Aplicació Nom Aplicació Codi Àrea Nom Àrea Codi Política Nom Política Codi Programa Nom Programa Import sense consolidar Import Consolidat Sector Públic
0 NaN 2020 D AG01 Gabinet i SG d'Ag.,Ram. Pesca i A Generalitat AG Agricultura, Ramaderia, Pesca i Alimentació AG Agricultura, Ramaderia, Pesca i Alimentació ... 1000001 Retribucions bàsiques 1.0 Funcionament de les institucions i administrac... 12.0 Administració i serveis generals 121.0 Direcció i administració generals 410625.63 410625.63
1 NaN 2020 D AG01 Gabinet i SG d'Ag.,Ram. Pesca i A Generalitat AG Agricultura, Ramaderia, Pesca i Alimentació AG Agricultura, Ramaderia, Pesca i Alimentació ... 1000002 Retribucions complementàries 1.0 Funcionament de les institucions i administrac... 12.0 Administració i serveis generals 121.0 Direcció i administració generals 425228.38 425228.38
2 NaN 2020 D AG01 Gabinet i SG d'Ag.,Ram. Pesca i A Generalitat AG Agricultura, Ramaderia, Pesca i Alimentació AG Agricultura, Ramaderia, Pesca i Alimentació ... 1100001 Retribucions bàsiques 1.0 Funcionament de les institucions i administrac... 12.0 Administració i serveis generals 121.0 Direcció i administració generals 140482.42 140482.42
3 NaN 2020 D AG01 Gabinet i SG d'Ag.,Ram. Pesca i A Generalitat AG Agricultura, Ramaderia, Pesca i Alimentació AG Agricultura, Ramaderia, Pesca i Alimentació ... 1100002 Retribucions complementàries 1.0 Funcionament de les institucions i administrac... 12.0 Administració i serveis generals 121.0 Direcció i administració generals 365418.47 365418.47
4 NaN 2020 D AG01 Gabinet i SG d'Ag.,Ram. Pesca i A Generalitat AG Agricultura, Ramaderia, Pesca i Alimentació AG Agricultura, Ramaderia, Pesca i Alimentació ... 1200001 Retribucions bàsiques 1.0 Funcionament de les institucions i administrac... 12.0 Administració i serveis generals 121.0 Direcció i administració generals 24780580.69 24780580.69

5 rows × 31 columns

In [5]:
#normalizamos los nombres de las columnas
def to_ascii(text):
    return ''.join(char if ord(char) < 128 else unicodedata.name(char)[:3] for char in text)

# Replace column names with ASCII equivalents
# df_raw.columns = [to_ascii(col) for col in df_raw.columns]
df_raw.columns = df_raw.columns.str.replace('/', '')
df_raw.columns = df_raw.columns.str.lower().str.replace(' ', '_')
df_raw.columns = df_raw.columns.str.lower().str.replace('__', '_')
In [6]:
# Visualizamos las columnas
print(df_raw.columns)
Index(['subjecteàmbit', 'exercici', 'ingrés_despesa', 'servei_entitat',
       'nom_servei_entitat', 'subsector', 'codi_agrupació', 'nom_agrupació',
       'codi_secció', 'nom_secció', 'tipus_de_secció',
       'entitat_cp_sector_públic', 'ordre_departamental', 'codi_entitat',
       'nom_entitat', 'capítol', 'nom_capítol', 'article', 'nom_article',
       'concepte', 'nom_concepte', 'aplicació', 'nom_aplicació', 'codi_àrea',
       'nom_àrea', 'codi_política', 'nom_política', 'codi_programa',
       'nom_programa', 'import_sense_consolidar',
       'import_consolidat_sector_públic'],
      dtype='object')

Filtramos por las columnas que nos interesan para la visualización, en este caso seleccionamos las columnas de 'nom_agrupació', 'subsector' y 'import_consolidat_sector_públic' y filtramos por el ejercicio 2023 y por los valores de 'ingrés_despesa' que sean 'D' (Despesa).

In [7]:
df_work = df_raw.query('exercici == 2023 and ingrés_despesa == "D" and import_consolidat_sector_públic > 0')

# selecciona las columnas de nom_servei_entitat, nom_agració, nom_programa, import_consolidat_sector_públic)
df_clean = df_work[['nom_agrupació','subsector','import_consolidat_sector_públic']]

df_clean.head()
Out[7]:
nom_agrupació subsector import_consolidat_sector_públic
44 Drets Socials Generalitat 30177165.22
110 Interior Generalitat 500.00
125 Drets Socials Generalitat 9676836.74
259 Drets Socials Generalitat 5596898.03
272 Drets Socials Generalitat 7500000.00
In [8]:
# numero de filas
print(f"Numero de filas: {df_clean.shape[0]: ,}")
Numero de filas:  11,893

Iniciamos la generación de las agregaciones.

In [9]:
# group by nom_servei_entitat, nom_agrupació, nom_programa and sum import_consolidat_sector_públic
df_grouped = df_clean.groupby(['nom_agrupació', 'subsector' ]).sum().reset_index()

# order by nom_servei_entitat, nom_agrupació, nom_programa
df_grouped = df_grouped.sort_values(['nom_agrupació', 'subsector' ])

df_grouped
Out[9]:
nom_agrupació subsector import_consolidat_sector_públic
0 Acció Climàtica, Alimentació I Agenda Rural Consorcis 1.939577e+07
1 Acció Climàtica, Alimentació I Agenda Rural EA administratives i CatSalut 3.872561e+06
2 Acció Climàtica, Alimentació I Agenda Rural Entitats dret públic 1.148284e+09
3 Acció Climàtica, Alimentació I Agenda Rural Fundacions 1.572711e+07
4 Acció Climàtica, Alimentació I Agenda Rural Generalitat 9.591502e+08
... ... ... ...
62 Territori Entitats dret públic 1.559871e+09
63 Territori Fundacions 7.031770e+05
64 Territori Generalitat 5.338767e+08
65 Territori Societats mercantils 8.900330e+08
66 Òrgans Superiors I Altres Generalitat 1.006592e+08

67 rows × 3 columns

In [10]:
# group by nom_servei_entitat, nom_agrupació, nom_programa and sum import_consolidat_sector_públic
df_grouped_2 = df_clean[['nom_agrupació','import_consolidat_sector_públic']].groupby(['nom_agrupació']).sum().reset_index()

# order by nom_servei_entitat, nom_agrupació, nom_programa
df_grouped_2 = df_grouped_2.sort_values(['nom_agrupació'])

df_grouped_2
Out[10]:
nom_agrupació import_consolidat_sector_públic
0 Acció Climàtica, Alimentació I Agenda Rural 2.183375e+09
1 Acció Exterior I Unió Europea 9.724492e+07
2 Cultura 4.937219e+08
3 Drets Socials 3.405248e+09
4 Economia I Hisenda 8.791779e+08
5 Educació 6.742125e+09
6 Empresa I Treball 1.494817e+09
7 Fons No Departamentals 6.703634e+09
8 Igualtat I Feminismes 1.145687e+08
9 Interior 1.814303e+09
10 Justícia, Drets I Memòria 1.142574e+09
11 Presidència 1.838788e+09
12 Recerca I Universitats 1.763675e+09
13 Salut 1.223284e+10
14 Territori 4.794202e+09
15 Òrgans Superiors I Altres 1.006592e+08
In [11]:
# Initialize an empty list to store the dictionaries
tree_1 = dict()

# Iterate over each group
for row in df_grouped.itertuples(index=False):
    # Create a dictionary for the parent
    if row[0] not in tree_1.keys():
        
        parent_dict = {
            'id': row[0],
            'children': [
                {
                    'id': row[1],
                    'datum': row[2]
                }
            ]

        }
        tree_1[row[0]] = parent_dict
    else:
        parent_dict = tree_1[row[0]]
        parent_dict['children'].append({
            'id': row[1],
            'datum': row[2]
        })
In [12]:
tree_2 = dict()

for row in df_grouped_2.itertuples(index=False):

    if row[0] not in tree_2.keys():

        parent_dict = {
            'id': row[0],
            'datum': row[1]
        }
        
        parent_dict['children'] = tree_1[row[0]]['children']
        
        tree_2[row[0]] = parent_dict
    else:
        parent_dict = tree_1[row[0]]
        parent_dict['children'].append({
            'id': row[1],
            'datum': row[2]
        })
In [13]:
tree_3 = list()

# creamos el dict jerarquico final
for i,val in tree_2.items():
    tree_3.append(val)
In [14]:
# Compute circle positions 
circles = circ.circlify(
    tree_3,
    show_enclosure=False,
    target_enclosure=circ.Circle(x=0, y=0, r=1)
)
In [15]:
fig, ax = plt.subplots(figsize=(14, 14))

# Title
ax.set_title('Distribución de los presupuestos de la Generalitat de Catalunya por Agrupación y Subsector')

# Remove axes
ax.axis('off')

# Find axis boundaries
lim = max(
    max(
        abs(circle.x) + circle.r,
        abs(circle.y) + circle.r,
    )
    for circle in circles
)
plt.xlim(-lim, lim)
plt.ylim(-lim, lim)

# Print circle the highest level (subsector
for circle in circles:
    if circle.level != 1:
        continue
    x, y, r = circle
    ax.add_patch(plt.Circle((x, y), r, alpha=0.5,
                            linewidth=2, color="lightblue"))

# Print circle and labels for the highest level:
for circle in circles:
    if circle.level != 2:
        continue
    x, y, r = circle
    label = circle.ex["id"]
    ax.add_patch(plt.Circle((x, y), r, alpha=0.5,
                            linewidth=2, color="#69b3a2"))
    plt.annotate(label, (x, y), ha='center', color='white')

# Print labels for the continents
for circle in circles:
    if circle.level != 1:
        continue
    x, y, r = circle
    label = circle.ex["id"]
    plt.annotate(label, (x, y), va='center', ha='center', bbox=dict(
        facecolor='white', edgecolor='black', boxstyle='round', pad=0.005))

Sunburst Chart¶

Descripción¶

De la misma forma que la gráfica anterior, los Sunburst chart nos permiten representar jerarquías anidadas, pero en este caso forma de círculos concéntricos. Los círculos más externos representan la jerarquía de nivel superior, mientras que los círculos internos representan los niveles jerárquicos inferiores. Cada nivel de jerarquía se codifica por colores y el tamaño de los círculos se correlaciona con un atributo o medida vinculada a ese nodo.

De forma también similar a los Packed Circle Chart, resulta difícil comparar los tamaños de los círculos, especialmente cuando son muy densos y también es difícil etiquetar los nodos en estos casos

Aplicación práctica¶

Para la visualización de los datos, utilizaremos los mismos datos que en la visualización anterior, pero en este caso utilizaremos columnas diferentes para la jerarquía.

In [16]:
# selecciona las columnas de nom_servei_entitat, nom_agració, nom_programa, import_consolidat_sector_públic)
df_clean_2 = df_work[['nom_agrupació','nom_capítol','import_consolidat_sector_públic']]

df_clean_2.head()
Out[16]:
nom_agrupació nom_capítol import_consolidat_sector_públic
44 Drets Socials Transferències de capital 30177165.22
110 Interior Despeses corrents de béns i serveis 500.00
125 Drets Socials Despeses corrents de béns i serveis 9676836.74
259 Drets Socials Transferències de capital 5596898.03
272 Drets Socials Transferències de capital 7500000.00
In [17]:
import plotly.express as px
import plotly.graph_objects as go
In [18]:
# group by nom_servei_entitat, nom_agrupació, nom_programa and sum import_consolidat_sector_públic
df_grouped_3 = df_clean_2.groupby(['nom_agrupació', 'nom_capítol' ]).sum().reset_index()

# order by nom_servei_entitat, nom_agrupació, nom_programa
df_grouped_3 = df_grouped_3.sort_values(['nom_agrupació', 'nom_capítol' ])

df_grouped_3['año'] = 2023

df_grouped_3
Out[18]:
nom_agrupació nom_capítol import_consolidat_sector_públic año
0 Acció Climàtica, Alimentació I Agenda Rural Aportacions de capital i préstecs 4.924159e+07 2023
1 Acció Climàtica, Alimentació I Agenda Rural Despeses corrents de béns i serveis 6.732715e+08 2023
2 Acció Climàtica, Alimentació I Agenda Rural Despeses de personal 2.861530e+08 2023
3 Acció Climàtica, Alimentació I Agenda Rural Deute 3.360370e+07 2023
4 Acció Climàtica, Alimentació I Agenda Rural Interessos 6.566419e+06 2023
... ... ... ... ...
110 Òrgans Superiors I Altres Aportacions de capital i préstecs 7.420000e+04 2023
111 Òrgans Superiors I Altres Despeses corrents de béns i serveis 1.734639e+07 2023
112 Òrgans Superiors I Altres Despeses de personal 6.714821e+07 2023
113 Òrgans Superiors I Altres Inversions 3.175329e+06 2023
114 Òrgans Superiors I Altres Transferències corrents 1.291511e+07 2023

115 rows × 4 columns

In [19]:
# Creamos la figura de sunburst
fig = px.sunburst(df_grouped_3, path=['año','nom_agrupació', 'nom_capítol'], values='import_consolidat_sector_públic', color='nom_agrupació', hover_data=['import_consolidat_sector_públic'], title='Distribución de los presupuestos de la Generalitat por Agrupación y Capítulo para el ejercicio 2023')


# Actualizamos el diseño para ajustar el margen
fig.update_layout(
    margin=dict(t=10, l=10, r=10, b=10),
)

# Actualizamos el texto para que sea radial
fig.update_traces(
    textinfo='label+percent entry',
    insidetextorientation='radial',
)

fig.show()

Ridgeline plot¶

Introducción¶

Un Ridgeline plot muestra la distribución de un valor numérico en diferentes grupos. Esta distribución puede ser utílizada mediante histogramas o gráficos de densidad, alineados en la misma escala horizontal con una ligera superposición.

Este gráfico es apropiado cuando existe un patron claro, ya que oculta parte la información que se superpone.

Aplicación práctica¶

Para este caso, usaremos el set de datos de la cantidad de agua en los embalses internos de catalunya desde el 2020 hasta el 2000, disponible en dades obertes de la Generalitat de Catalunya.

https://analisi.transparenciacatalunya.cat/Medi-Ambient/Quantitat-d-aigua-als-embassaments-de-les-Conques-/gn9e-3qhr/data_preview

In [20]:
# Cargamos el dataset
df_raw = pd.read_csv('Quantitat_d_aigua_als_embassaments_de_les_Conques_Internes_de_Catalunya_20240422.csv')
In [21]:
df_raw.head()
Out[21]:
Dia Estació Nivell absolut (msnm) Percentatge volum embassat (%) Volum embassat (hm3)
0 21/04/2024 Embassament de Darnius Boadella (Darnius) 132.40 11.5 7.03
1 21/04/2024 Embassament de Riudecanyes 194.74 2.8 0.15
2 21/04/2024 Embassament de Susqueda (Osor) 304.42 24.0 56.02
3 21/04/2024 Embassament de Sant Ponç (Clariana de Cardener) 514.45 32.6 7.94
4 21/04/2024 Embassament de la Llosa del Cavall (Navès) 769.31 20.4 16.30
In [22]:
# Nomralizamos los nombres de las columnas
df_raw.columns = ['dia','estacion','nivel_abs','porcentaje_volumen','volumen_embasado_hm3']

# Cast dia to datetime
df_raw['dia'] = pd.to_datetime(df_raw['dia'], format='%d/%m/%Y')
In [23]:
# Visualizamos las columnas
print(df_raw.columns)

print(f"Numero de filas: {df_raw.shape[0]: ,}")
Index(['dia', 'estacion', 'nivel_abs', 'porcentaje_volumen',
       'volumen_embasado_hm3'],
      dtype='object')
Numero de filas:  79,902
In [24]:
# get min and max date
min_date = df_raw['dia'].min()
max_date = df_raw['dia'].max()

print(f"Min date: {min_date}")
print(f"Max date: {max_date}")
Min date: 2000-01-01 00:00:00
Max date: 2024-04-21 00:00:00
In [25]:
# Filtramos los datos desde el 2010
df_work = df_raw.query('dia >= "01/01/2013" and dia <= "31/12/2023"')

min_date = df_work['dia'].min()
max_date = df_work['dia'].max()

print(f"Min date: {min_date}")
print(f"Max date: {max_date}")


df_work
Min date: 2013-01-01 00:00:00
Max date: 2023-12-31 00:00:00
Out[25]:
dia estacion nivel_abs porcentaje_volumen volumen_embasado_hm3
1008 2023-12-31 Embassament de la Baells (Cercs) 593.56 22.2 24.35
1009 2023-12-31 Embassament de Susqueda (Osor) 300.21 20.4 47.43
1010 2023-12-31 Embassament de Foix (Castellet i la Gornal) 97.32 53.8 2.01
1011 2023-12-31 Embassament de Sau (Vilanova de Sau) 380.85 8.0 13.15
1012 2023-12-31 Embassament de la Llosa del Cavall (Navès) 766.56 17.7 14.18
... ... ... ... ... ...
37156 2013-01-01 Embassament de Riudecanyes 213.44 59.8 3.18
37157 2013-01-01 Embassament de Darnius Boadella (Darnius) 146.87 44.8 27.37
37158 2013-01-01 Embassament de Susqueda (Osor) 331.29 57.9 134.81
37159 2013-01-01 Embassament de Sau (Vilanova de Sau) 405.27 44.4 73.43
37160 2013-01-01 Embassament de Siurana (Cornudella de Montsant) 481.25 74.0 9.04

36153 rows × 5 columns

In [26]:
# creamos una columna para el año
df_work['año'] = pd.to_datetime(df_work['dia']).dt.year

df_work.head()
Out[26]:
dia estacion nivel_abs porcentaje_volumen volumen_embasado_hm3 año
1008 2023-12-31 Embassament de la Baells (Cercs) 593.56 22.2 24.35 2023
1009 2023-12-31 Embassament de Susqueda (Osor) 300.21 20.4 47.43 2023
1010 2023-12-31 Embassament de Foix (Castellet i la Gornal) 97.32 53.8 2.01 2023
1011 2023-12-31 Embassament de Sau (Vilanova de Sau) 380.85 8.0 13.15 2023
1012 2023-12-31 Embassament de la Llosa del Cavall (Navès) 766.56 17.7 14.18 2023
In [27]:
# group by año and mean porcentaje_volumen
año_temp_serie = df_work.groupby(['año'])['porcentaje_volumen'].mean()

año_temp_serie
Out[27]:
año
2013    82.810837
2014    83.002314
2015    80.212664
2016    65.626260
2017    64.983775
2018    69.862527
2019    74.700609
2020    87.505647
2021    73.161096
2022    46.492968
2023    25.543105
Name: porcentaje_volumen, dtype: float64
In [28]:
df_work['media_porcentaje_volumen'] = df_work['año'].map(año_temp_serie)

df_work.head()
Out[28]:
dia estacion nivel_abs porcentaje_volumen volumen_embasado_hm3 año media_porcentaje_volumen
1008 2023-12-31 Embassament de la Baells (Cercs) 593.56 22.2 24.35 2023 25.543105
1009 2023-12-31 Embassament de Susqueda (Osor) 300.21 20.4 47.43 2023 25.543105
1010 2023-12-31 Embassament de Foix (Castellet i la Gornal) 97.32 53.8 2.01 2023 25.543105
1011 2023-12-31 Embassament de Sau (Vilanova de Sau) 380.85 8.0 13.15 2023 25.543105
1012 2023-12-31 Embassament de la Llosa del Cavall (Navès) 766.56 17.7 14.18 2023 25.543105
In [29]:
# get unique years in df
years = df_work['año'].unique()

#sort years in asc order
years = np.sort(years)[::1].tolist()

years
Out[29]:
[2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023]
In [31]:
# we generate a color palette with Seaborn.color_palette()
pal = sns.color_palette(n_colors=10)

# in the sns.FacetGrid class, the 'hue' argument is the one that is the one that will be represented by colors with 'palette'
g = sns.FacetGrid(df_work, row='año', hue='media_porcentaje_volumen', aspect=15, height=0.75, palette=pal)

# then we add the densities kdeplots for each month
g.map(sns.kdeplot, 'porcentaje_volumen',
      bw_adjust=1, clip_on=False,
      fill=True, alpha=1, linewidth=1.5)


# here we add a horizontal line for each plot
g.map(plt.axhline, y=0,
      lw=2, clip_on=False)



plt.setp(ax.get_xticklabels(), fontsize=15, fontweight='bold')
plt.xlabel('% de volumen de embalses', fontweight='bold', fontsize=15)
g.fig.suptitle('Media diaria del % de volumen de embalses por año',
               ha='right',
               fontsize=20,
               fontweight=20)

plt.show()